'''
List-like types.
'''
import collections
import weakref
from pyson.json_encoding import register_cls

class View(object):
    attrs = None
    length = None
    viewtype = object

    def __init__(self, obj):
        if not isinstance(obj, self.viewtype):
            t1 = self.viewtype.__name__
            t2 = type(obj).__name__
            raise TypeError("object of type '%s' expected, got '%s'" % (t1, t2))

        self._data = obj

    @classmethod
    def subclass(cls, viewtype, attrs):
        '''Creates a new subclass of view'''

        attrs = tuple(map(str, attrs))
        if isinstance(viewtype, type):
            name = viewtype.__name__ + 'View'
        else:
            viewtype, name = object, viewtype
        return type(name, (cls,), {'attrs': attrs, 'length': len(attrs), 'viewtype': object})

    #===========================================================================
    # Support for Json Encoding
    #===========================================================================
    def __json_encode__(self):
        return self.__dict__

    @classmethod
    def __json_decode__(cls, json):
        new = object.__new__(cls)
        new.__dict__.update(json)
        return new

@register_cls('pyson.SeqView')
class SeqView(collections.MutableSequence, View):
    '''SeqView implements a view for the attributes in a Python object as a 
    sequence.
    
    This is useful when the attributes can be put naturally in a sequential 
    order. Consider the example of a user created Point() class:
    
    >>> class Point(object):
    ...     def __init__(self, x, y):
    ...         self.x, self.y = x, y
    ...
    ...     # ... rest of implementation
    
    We may want a view to a Point object that behaves as the sequence (x, y). 
    For that, we can use the subclass method of SeqView that creates a new 
    class that implements this interface: 
    
    >>> PointView = SeqView.subclass(Point, ['x', 'y'])
    
    Now, instances of PointView() can be manipulated as a 2 element sequence. 
    Changes in the view are propagated to the original object and vice-versa: 
    
    >>> pt = Point(1, 2)
    >>> pview = PointView(pt); pview
    PointView([1, 2])
    
    >>> pview[0], pt.y = 0, 0
    >>> pt.x, pt.y, pview
    (0, 0, PointView([0, 0]))
    '''
    def __init__(self, obj):
        if type(self) is SeqView:
            raise TypeError('must instantiate SeqView subclasses')
        else:
            View.__init__(self, obj)

    def __delitem__(self, idx):
        raise TypeError('cannot change size')

    def __getitem__(self, idx):
        attr = self.attrs[idx]
        try:
            return getattr(self._data, attr)
        except AttributeError:
            raise IndexError('%s (%s)' % (idx, attr))

    def __len__(self):
        return self.length

    def __setitem__(self, idx, value):
        attr = self.attrs[idx]
        try:
            return setattr(self._data, attr, value)
        except AttributeError:
            raise IndexError('%s (%s)' % (idx, attr))

    def insert(self, idx, value):
        raise TypeError('cannot change size')

    def __repr__(self):
        return '%s([%s])' % (type(self).__name__, (', '.join(repr(x) for x in self)))

@register_cls('pyson.MapView')
class MapView(collections.MutableMapping, View):
    '''MapView implements a view for the attributes in a Python object as a 
    dictionary.
    
    Consider the example of a user created Point() class:
    
    >>> class Point(object):
    ...     def __init__(self, x, y):
    ...         self.x, self.y = x, y
    ...
    ...     # ... rest of implementation
    
    A view of Point object that behaves like the dictionary 
    {'x': Point.x, 'y': Point.y} can be implemented explicitly in a subclass of
    MapView.
    
    >>> PointView = MapView.subclass(Point, ['x', 'y'])
    
    Now, instances of PointView() can be manipulated as a dictionary with both
    keys: 
    
    >>> pt = Point(1, 2)
    >>> pview = PointView(pt); pview
    PointView({'x': 1, 'y': 2})
    
    >>> pview['x'], pt.y = 0, 0
    >>> (pt.x, pt.y), pview
    ((0, 0), PointView({'x': 0, 'y': 0}))
    
    We can also add new attributes
    
    >>> pview['z'] = 0
    >>> pt.x, pt.y, pt.z
    (0, 0, 0)
    >>> pview == {'x': 0, 'y': 0, 'z': 0}
    True
    
    MapView tries its best to infer the attributes of an object even when the
    viewer is not created explicitly with MapView.subclass(). 
    
    >>> MapView(1 + 2j)
    MapView({'real': 1.0, 'imag': 2.0})
    '''
    _viewtypes_keys = weakref.WeakKeyDictionary()

    def _type_keys(self, tt):
        keys_dict = self._viewtypes_keys
        try:
            return keys_dict[tt]
        except KeyError:
            keys = [k for (k, v) in vars(tt).items() if hasattr(v, '__get__') and hasattr(v, '__set__')]
            keys.extend(getattr(tt, '__slots__', []))
            keys = set(k for k in keys if not k.startswith('_'))
            keys_dict[tt] = keys
            return keys

    def _own_attrs(self):
        if 'attrs' not in self.__dict__:
            self.attrs = set(self.attrs)

    def __delitem__(self, key):
        try:
            delattr(self._data, key)
        except AttributeError:
            raise KeyError(key)
        else:
            if self.attrs is not None:
                self._own_attrs()
                self.attrs.discard(key)

    def __getitem__(self, key):
        try:
            value = getattr(self._data, key)
        except AttributeError:
            raise KeyError(key)
        else:
            if self.attrs is not None and key not in self.attrs:
                self._own_attrs()
                self.attrs.add(key)
            return value

    def __setitem__(self, key, value):
        key = str(key)
        try:
            setattr(self._data, key, value)
        except AttributeError:
            raise KeyError(key)
        else:
            if self.attrs is not None and key not in self.attrs:
                self._own_attrs()
                self.attrs.add(key)

    def __iter__(self):
        return (k for (k, v) in self.iteritems())

    def __len__(self):
        if self.attrs is None:
            return sum(1 for _ in self)
        else:
            return len(self.attrs)

    def __repr__(self):
        return '%s({%s})' % (type(self).__name__, (', '.join('%s: %s' % (repr(x), repr(y)) for (x, y) in self.iteritems())))

    def iteritems(self):
        data = self._data
        if self.attrs is None:
            attrs = iter(self._type_keys(type(data)))
            try:
                for k, v in data.__dict__.iteritems():
                    if not k.startswith('_'):
                        yield (k, v)
            except AttributeError:
                pass
        else:
            attrs = iter(self.attrs)

        for attr in attrs:
                try:
                    yield attr, getattr(data, attr)
                except AttributeError:
                    raise KeyError("expected attribute in data does not exist: '%s'" % attr)
    iteritems.__doc__ = dict.iteritems.__doc__

    def copy(self):
        from copy import copy

        new = type(self)(copy(self._data))
        if 'attrs' in self.__dict__:
            new.attrs = set(self.attrs)
        return new
    copy.__doc__ = dict.copy.__doc__

class DataDelegate(object):
    import operator as op

    # Binary operators ---------------------------------------------------------
    def bin_op(op): #@NoSelf
        def factory_method(self, other):
            cls = type(self)
            other = other.data if isinstance(other, DataDelegate) else other
            return cls(op(self._data, other))
        return factory_method

    # Arithmetical operators
    __add__, __sub__, __mul__, __div__, __pow__ = \
        map(bin_op, [op.add, op.sub, op.mul, op.div, op.pow])
    __radd__ = bin_op(lambda x, y: y + x)
    __rsub__ = bin_op(lambda x, y: y - x)
    __rmul__ = bin_op(lambda x, y: y * x)
    __rdiv__ = bin_op(lambda x, y: y / x)

    # Logical operators

    del bin_op

    for k, v in locals().items():
        name = getattr(v, '__name__', None)
        if name == 'factory_method':
            v.__name__ = k

class AttrView(object):
    '''Return a view of a mapping as a Python object in which keys are 
    accessible as attributes.
    
    >>> view = AttrView({'x': 1, 'y': 2})
    >>> view.x, view.y
    (1, 2)
    '''

    def __init__(self, data):
        if isinstance(data, collections.Mapping):
            object.__setattr__(self, '_data', data)
        else:
            raise TypeError("'data' must be a mapping")

    def __getattr__(self, attr):
        try:
            return self._data[attr]
        except KeyError:
            raise AttributeError(attr)

    def __setattr__(self, attr, value):
        try:
            self._data[attr] = value
        except KeyError:
            raise AttributeError(attr)

class RAttrView(AttrView):
    '''Return a view of a mapping as a Python object in which keys are 
    accessible as attributes. This class differs from ObjView in which it is 
    recursive, i.e., items that are mappings are also returned as views. 
    
    >>> view = RAttrView({'foo': 'bar', 'shop_list': {'spam': 1, 'eggs': 12}})
    >>> view.foo
    'bar'
    >>> (view.shop_list.spam, view.shop_list.eggs) 
    (1, 12)
    '''
    def __getattr__(self, attr):
        try:
            value = self._data[attr]
            if isinstance(value, collections.Mapping):
                return type(self)(value)
            else:
                return value
        except KeyError:
            raise AttributeError(attr)

class EncodingSeq(collections.MutableSequence):
    '''Encodes objects before storing them in sequence and decodes them 
    before presenting to user.
    
    >>> raw_data = [1, 2, 3, 4]
    >>> shifted = EncodingSeq(raw_data, lambda x: x - 2, lambda x: x + 2)
    >>> list(shifted)
    [3, 4, 5, 6]
    
    Changes in the encoded list are passed to the original data
    
    >>> shifted[:] = [1, 2]
    >>> list(shifted), raw_data
    ([1, 2], [-1, 0])
    '''

    def __init__(self, data, encoder=None, decoder=None):
        self._data = data
        self.encoder = (encoder if encoder is not None else lambda x: x)
        self.decoder = (decoder if decoder is not None else lambda x: x)

    def __delitem__(self, idx):
        del self._data[idx]

    def __getitem__(self, idx):
        return self.decoder(self._data[idx])

    def __len__(self):
        return len(self._data)

    def __setitem__(self, idx, value):
        self._data[idx] = self.encoder(value)

    def insert(self, idx, value):
        self._data.insert(idx, self.encoder(value))

    def __getslice__(self, i, j):
        return EncodingSeq(self._data[i:j], self.encoder, self.decoder)

    def __setslice__(self, i, j, value):
        self._data[i:j] = [ self.encoder(x) for x in value ]

    @property
    def data(self):
        return self._data

if __name__ == '__main__':
    print MapView(1 + 2j)
    class Point(object):
        def __init__(self, x, y):
            self.x, self.y = x, y

    pt = Point(1, 2)
    PtView = MapView.subclass(Point, ['x', 'y'])
    pview = PtView(pt)
    print pview
    pview['z'] = 1
    print pview
    print pt.__dict__
    import doctest
    doctest.testmod(optionflags=doctest.REPORT_ONLY_FIRST_FAILURE, verbose=0)


